// Package mongodb alertmanager.yaml configure to mongodb
// https://github.com/mongodb/mongo-go-driver
package mongodb

import (
	"context"
	"errors"
	"time"

	"go.mongodb.org/mongo-driver/mongo/readpref"

	"github.com/go-kit/kit/log/level"
	"github.com/prometheus/alertmanager/aldap"

	"github.com/go-kit/kit/log"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

// ZA: mongodb configure
const (
	UserCollection  = "user"
	GroupCollection = "receiver"
	ItemCollection  = "route"
)

type AlertReceiver struct {
	UserGroup  string   `json:"usergroup"`
	Notifiers  []string `json:"notifiers"`
	UserEmails []string `json:"email"`
}

type AlertRoute struct {
	AlertGroup string `json:"alertgroup"`
	AlertName  string `json:"alertname"`
	From       string `json:"from"`
	UserGroup  string `json:"usergroup"`
}

type AlertUser struct {
	// ID    primitive.ObjectID `bson:"_id" json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
	Phone string `json:"phone"`
}

type AlertCfgReceiver struct {
	Receiver string       `json:"receiver,omitempty"`
	MatchRE  AlertMatchRE `json:"match_re,omitempty"`
}

type AlertMatchRE struct {
	Alertname string `json:"alertname,omitempty"`
	From      string `json:"from,omitempty"`
}

type AlertMongo struct {
	DBAlert *mongo.Database
	Ctx     context.Context
	Cancel  context.CancelFunc
	Logger  log.Logger
}

// var MongoAlert *AlertMongo

// NewMongoClient connect to mongo db
func NewMongoClient(url string, db string, logger log.Logger) (mogClient *AlertMongo, err error) {

	ctx, cancel := context.WithCancel(context.Background())
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(url))
	alt := &AlertMongo{DBAlert: client.Database(db), Ctx: ctx, Cancel: cancel, Logger: logger}

	// test ping
	ctx1, cancel1 := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel1()
	err = client.Ping(ctx1, readpref.Primary())
	if err != nil {
		level.Error(logger).Log("msg", "connect mongodb fail")
		return nil, errors.New(url + " connect timeout")
	}

	level.Info(logger).Log("msg", "connect mongodb success")
	return alt, nil
}

// QueryAllUser query user collection
func (am *AlertMongo) QueryAllUser() (users []*AlertUser, err error) {
	coll := am.DBAlert.Collection(UserCollection)
	cur, err := coll.Find(am.Ctx, bson.D{})
	if err != nil {
		return nil, err
	}

	for cur.Next(am.Ctx) {
		var user AlertUser
		err := cur.Decode(&user)
		if err != nil {
			level.Error(am.Logger).Log("msg", "Error mongo alert user", "err", err)
			continue
		}
		users = append(users, &user)
	}

	if cur.Err() != nil {
		return nil, cur.Err()
	}

	return users, nil
}

// QueryAllGroup query receiver collection
func (am *AlertMongo) QueryAllGroup() (receivers []*AlertReceiver, err error) {
	collection := am.DBAlert.Collection(GroupCollection)
	cur, err := collection.Find(am.Ctx, bson.D{})
	if err != nil {
		return receivers, err
	}

	for cur.Next(am.Ctx) {
		var receiver AlertReceiver
		err := cur.Decode(&receiver)
		if err != nil {
			level.Error(am.Logger).Log("msg", "Error mongo alert receiver group", "err", err)
			continue
		}
		receivers = append(receivers, &receiver)
	}

	if cur.Err() != nil {
		return receivers, cur.Err()
	}

	return receivers, nil
}

// QueryAllItem query alert item collection
func (am *AlertMongo) QueryAllItem() (routes []*AlertRoute, err error) {
	coll := am.DBAlert.Collection(ItemCollection)
	cur, err := coll.Find(am.Ctx, bson.D{})
	if err != nil {
		return routes, err
	}

	for cur.Next(am.Ctx) {
		var route AlertRoute
		err := cur.Decode(&route)
		if err != nil {
			level.Error(am.Logger).Log("msg", "Error mongo alert routes", "err", err)
			continue
		}
		routes = append(routes, &route)
	}

	if cur.Err() != nil {
		return routes, cur.Err()
	}

	return routes, nil
}

// QueryOneGroup ...
func (am *AlertMongo) QueryOneGroup(filter interface{}) (t AlertReceiver, err error) {
	coll := am.DBAlert.Collection(GroupCollection)
	err = coll.FindOne(am.Ctx, filter).Decode(&t)
	if err != nil {
		level.Info(am.Logger).Log("msg", "find group fail", "err", err)
		return t, err
	}
	return t, nil
}

// QueryOneUser ...
func (am *AlertMongo) QueryOneUser(filter interface{}) (t AlertUser, err error) {
	coll := am.DBAlert.Collection(UserCollection)
	err = coll.FindOne(am.Ctx, filter).Decode(&t)
	if err != nil {
		level.Info(am.Logger).Log("msg", "find user fail", "err", err)
		return t, err
	}
	return t, nil
}

// QueryOne ...
func (am *AlertMongo) QueryOne(collectionName string, filter interface{}) *mongo.SingleResult {
	collection := am.DBAlert.Collection(collectionName)
	return collection.FindOne(am.Ctx, filter)
}

// InsertOne ...
func (am *AlertMongo) InsertOne(collection string, data interface{}) (ir *mongo.InsertOneResult, err error) {
	coll := am.DBAlert.Collection(collection)
	ir, err = coll.InsertOne(am.Ctx, data)
	if err != nil {
		return nil, err
	}
	return ir, err
}

// UpdateOne ...
func (am *AlertMongo) UpdateOne(collection string, filter interface{}, data interface{}) (n int64, err error) {
	coll := am.DBAlert.Collection(collection)
	ur, err := coll.UpdateOne(am.Ctx, filter, data)
	if err != nil {
		return 0, err
	}
	return ur.ModifiedCount, nil
}

// DeleteOne ...
func (am *AlertMongo) DeleteOne(collection string, filter interface{}) (dr *mongo.DeleteResult, err error) {
	coll := am.DBAlert.Collection(collection)
	dr, err = coll.DeleteOne(am.Ctx, filter)
	if err != nil {
		return nil, err
	}
	return dr, nil
}

// BatchSyncUserByLDAP ...
func (am *AlertMongo) BatchSyncUserByLDAP(url string, port int, username string, password string, basedn string) {
	cldap := aldap.NewLDAP(url, port, username, password)
	defer cldap.Close()

	entries, err := cldap.GetEntries(basedn)
	if err != nil {
		level.Error(am.Logger).Log("msg", "ERROR batch sync user to mongodb", err)
		return
	}

	for _, entry := range entries {
		name := entry.GetAttributeValue("cn")
		email := entry.GetAttributeValue("uid")
		user := &AlertUser{
			Name:  name,
			Email: email,
		}

		am.InsertOne("user", user)
	}
}

// Close ...
func (am *AlertMongo) Close() {
	am.Cancel()
}
